package de.upb.aquarium.water;

import java.io.File;
import java.io.IOException;

import javax.media.opengl.GL;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUnurbs;

import com.sun.opengl.cg.CgGL;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureIO;

import de.upb.aquarium.World;
import de.upb.aquarium.api.objects.General3DObject;
import de.upb.aquarium.api.shader.WaterPerPixelShader;
import de.upb.aquarium.api.utility.VectorDouble;
import de.upb.aquarium.api.utility.VectorFloat;

public class Surface extends General3DObject {
	
	private double startTime;
	
	private int horizontalResolution, verticalResolution;
	private double width, depth;
	private VectorDouble surfaceNormal;
	
	private int texHeight, texWidth;
	
	private float[] controlPoints;
	private float[] knotsHorizontal;
	private float[] knotsVertical;
	private GLUnurbs nurbs;
	
	private TextureRenderer reflectionTexture;
	private double[] reflectionCoords;
	
	private TextureRenderer refractionTexture;
	private double[] refractionCoords;
	
	private Texture bumpMap;
	private float bumpOriginX, bumpOriginY;
	private float bumpSpeedX, bumpSpeedY;
	
	private WaveGenerator waveGenerator;

	private WaterPerPixelShader shader;
	
	private boolean render;
	private boolean renderControlPoints;
	
	public Surface(double width, double depth,
				   int horizontalResolution, int verticalResolution,
				   WaveGenerator waveGenerator) throws GLException, IOException {
		
		this.horizontalResolution = horizontalResolution;
		this.verticalResolution = verticalResolution;
		
		this.width = width;
		this.depth = depth;
		
		this.surfaceNormal = new VectorDouble(new double[] {0.0, 0.0, 1.0});
		
		this.texWidth = 1024;
		this.texHeight = 2048;
	
		bumpMap = TextureIO.newTexture(new File("./textures/SmallWavesBump.bmp"), false);
		bumpOriginX = 0.0f;
		bumpOriginY = 0.0f;
		bumpSpeedX	= 0.00001f;
		bumpSpeedY	= 0.00001f;
		
		shader = new WaterPerPixelShader(CgGL.cgCreateContext());
		
		shader.setAmbientAbsorption(0.0f, 0.0f, 0.0f);
		shader.setDiffuseAbsorption(0.2f, 0.2f, 0.2f);
		shader.setSpecularAbsorption(0.5f, 0.5f, 0.5f, 128.0f);
		
		this.waveGenerator = waveGenerator;
		
		initializeNurbs();
		initializeTextures();
		
		render = true;
		renderControlPoints = false;
	}

	private void initializeTextures() {
		GL gl = World.getInstance().getGL();
		
		reflectionTexture = new TextureRenderer(gl, texWidth, texHeight);
		reflectionCoords = new double[12];
		
		refractionTexture = new TextureRenderer(gl, texWidth, texHeight);
		refractionCoords = new double[12];
	}

	private void initializeNurbs()
	{
		controlPoints = new float[3*horizontalResolution*verticalResolution];
		
		// Initialize the constant parts (i.e. the first 2 coordinates)
		// of the control points.
		for (int j = 0; j < verticalResolution; j++)
		{
			for (int i = 0; i < horizontalResolution; i++)
			{
				int index = indexOf(j,i);
				float x = (float)((i*width)/(horizontalResolution-1));
				float y = (float)((j*depth)/(verticalResolution-1));
				controlPoints[index + 0] = x;
				controlPoints[index + 1] = y;
			}
		}
				
		// Intialize knots.
		knotsHorizontal = new float[horizontalResolution + 3 + 1]; // Use cubic splines
		knotsVertical = new float[verticalResolution + 3 + 1];
		
		knotsHorizontal[0] = 0.0f;
		knotsHorizontal[1] = 0.0f;
		knotsHorizontal[horizontalResolution + 2] = 1.0f;
		knotsHorizontal[horizontalResolution + 3] = 1.0f;
		for (int i = 2; i < horizontalResolution + 2; i++)
		{
			knotsHorizontal[i] = ((float)(i-1))/((float)horizontalResolution + 1.0f);
		}
		
		knotsVertical[0] = 0.0f;
		knotsVertical[1] = 0.0f;
		knotsVertical[verticalResolution + 2] = 1.0f;
		knotsVertical[verticalResolution + 3] = 1.0f;
		for (int i = 2; i < verticalResolution + 2; i++)
		{
			knotsVertical[i] = ((float)(i-1))/((float)verticalResolution + 1.0f);
		}
		
		nurbs = World.getInstance().getGLU().gluNewNurbsRenderer();
	}

	public VectorDouble computeReflectionCameraPosition(
			VectorDouble cameraPositionWorld) {
		VectorDouble position = new VectorDouble(getPosition());
		double lambda;
		
		lambda = position.dot(surfaceNormal) - cameraPositionWorld.dot(surfaceNormal);
		lambda /= surfaceNormal.dot(surfaceNormal);
		
		return cameraPositionWorld.add(surfaceNormal.mul(2.0*lambda));
	}

	public VectorDouble computeReflectionCameraDirection(
			VectorDouble cameraDirectionWorld) {
		
		double gamma = cameraDirectionWorld.dot(surfaceNormal);
		
		return cameraDirectionWorld.sub(surfaceNormal.mul(2.0*gamma));
	}
	
	public void setClippingPlane(int planeIdent) {
		GL gl = World.getInstance().getGL();
		
		// Calculate the plane (in object coordinates!):
		double[] plane = new double[4];
		
		plane[0] = 0.0;
		plane[1] = 0.0;
		plane[2] = 1.0;
		plane[3] = 0.0;

		// Calculate ModelView matrix for this thing.
		super.beforeRendering(gl);
		
		gl.glClipPlane(planeIdent, plane, 0);
		
		super.afterRendering(gl);
	}

	public void grabReflectionMap() {
		reflectionTexture.copyFrameBufferToTexture(World.getInstance().getGL());
		calculateEdgeTextureCoordinates(reflectionCoords);
	}

	public void grabRefractionMap() {
		refractionTexture.copyFrameBufferToTexture(World.getInstance().getGL());
		calculateEdgeTextureCoordinates(refractionCoords);
	}
	
	private void calculateEdgeTextureCoordinates(double[] target)
	{
		GL gl = World.getInstance().getGL();
		GLU glu = World.getInstance().getGLU();
		
		double[] matrices = new double[32];
		int[] viewport = new int[4];
		
		gl.glGetDoublev(GL.GL_MODELVIEW_MATRIX, matrices, 0);
		gl.glGetDoublev(GL.GL_PROJECTION_MATRIX, matrices, 16);
		gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);

		glu.gluProject(0.0, depth, 0.0, matrices, 0, matrices, 16, viewport, 0, target, 0);
		glu.gluProject(0.0, 0.0, 0.0, matrices, 0, matrices, 16, viewport, 0, target, 3);
		glu.gluProject(width, depth, 0.0, matrices, 0, matrices, 16, viewport, 0, target, 6);
		glu.gluProject(width, 0.0, 0.0, matrices, 0, matrices, 16, viewport, 0, target, 9);
		
		for (int i = 0; i < 12; i += 3)
		{
			target[i + 0] /= texHeight*target[i+2];
			target[i + 1] /= texWidth*target[i+2];
			target[i + 2]  = 1.0/target[i+2];
		}
	}
	
	double timer = 0.0;
	
	@Override
	protected void beforeRendering(GL gl) {
		super.beforeRendering(gl);

		float elapsedTime = (float)World.getInstance().getElapsedTime(startTime);

		bumpOriginX = (bumpOriginX + elapsedTime*bumpSpeedX) % 1.0f;
		bumpOriginY = (bumpOriginY + elapsedTime*bumpSpeedY) % 1.0f;
		
		gl.glDisable(GL.GL_CULL_FACE);
		// Get refraction map.
		grabRefractionMap();
		refractionTexture.bindTexture(gl);
		
		fillHeights();
					
		shader.setLights(new VectorFloat(World.getInstance().getCameraPosition().data));
		
		int[] viewport = new int[4];
		gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
		shader.setTextureParameters(texWidth, texHeight,
				viewport[2] - viewport[0],
				viewport[3] - viewport[1]);
		
		shader.setRefractionTexture(refractionTexture.getTextureObject());
		shader.setDistanceToRefraction(0.2f);
//		shader.setRefractionTextureMapping(
//				refractionCoords[0], refractionCoords[1], refractionCoords[2],
//				refractionCoords[3], refractionCoords[4], refractionCoords[5],
//				refractionCoords[6], refractionCoords[7], refractionCoords[8],
//				refractionCoords[9], refractionCoords[10], refractionCoords[11]);
		
		shader.setReflectionTexture(reflectionTexture.getTextureObject());
		shader.setDistanceToReflection(0.2f);
//		shader.setReflectionTextureMapping(
//				reflectionCoords[0], reflectionCoords[1], reflectionCoords[2],
//				reflectionCoords[3], reflectionCoords[4], reflectionCoords[5],
//				reflectionCoords[6], reflectionCoords[7], reflectionCoords[8],
//				reflectionCoords[9], reflectionCoords[10], reflectionCoords[11]);
		
		shader.setBumpMap(
				bumpMap.getTextureObject(),
				bumpOriginX, bumpOriginY,
				bumpMap.getImageWidth(), bumpMap.getImageHeight());
		
		shader.setDimension(width, depth);
		
		shader.setSurfaceNormal(0.0, 0.0, 1.0);
		shader.activate();
	}
	
	@Override
	protected void afterRendering(GL gl) {
		shader.deactivate();
		gl.glEnable(GL.GL_CULL_FACE);
		gl.glDisable(GL.GL_TEXTURE_2D);
		super.afterRendering(gl);
	}

	@Override
	public void paint(GL gl) {
		if (!render)
		{
			return;
		}
		
		beforeRendering(gl);
		
		gl.glEnable(GL.GL_AUTO_NORMAL);
		gl.glEnable(GL.GL_NORMALIZE);
		
		GLU glu = World.getInstance().getGLU();
		
		glu.gluBeginSurface(nurbs);
	
			glu.gluNurbsSurface(
					nurbs,
					knotsHorizontal.length, knotsHorizontal,
					knotsVertical.length, knotsVertical,
					3, 3*horizontalResolution,
					controlPoints,
					4, 4,
					GL.GL_MAP2_VERTEX_3);
		glu.gluEndSurface(nurbs);

		gl.glDisable(GL.GL_NORMALIZE);
		gl.glDisable(GL.GL_AUTO_NORMAL);
	
		// Uncomment this for rendering the control points:
		
		if (renderControlPoints)
		{
			shader.deactivate();
			gl.glDisable(GL.GL_LIGHTING);
			
			gl.glPointSize(2.0f);
			gl.glBegin(GL.GL_POINTS);
				gl.glColor3f(1.0f, 0.0f, 0.0f);
				
				for (int i = 0; i < controlPoints.length; i += 3)
				{
					gl.glVertex3fv(controlPoints, i);
				}
			gl.glEnd();
			gl.glEnable(GL.GL_LIGHTING);
		}
			
		afterRendering(gl);
	}

	private int indexOf(int x, int y)
	{
		return 3*(x*horizontalResolution + y);
	}
	
	private void fillHeights()
	{
		final float[] heigths = new float[horizontalResolution*verticalResolution];
		
		waveGenerator.fillHeights(heigths, horizontalResolution);
		
		for (int i = 0; i < horizontalResolution; i++)
		{
			for (int j = 0; j < verticalResolution; j++)
			{
				controlPoints[indexOf(i, j) + 2] = heigths[j*horizontalResolution + i];
			}
		}
	}
	
	public void drawDiagnosisInfo(double x, double y, double z, double width, double height, double space)
	{
		GL gl = World.getInstance().getGL();
		
		// Refraction:
		gl.glEnable(GL.GL_TEXTURE_2D);
		gl.glDisable(GL.GL_LIGHTING);
		refractionTexture.bindTexture(gl);

		gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2d(0.0, 1.0); gl.glVertex3d(x,         y,          z);
			gl.glTexCoord2d(0.0, 0.0); gl.glVertex3d(x,         y - height, z);
			gl.glTexCoord2d(1.0, 0.0); gl.glVertex3d(x + width, y - height, z);
			gl.glTexCoord2d(1.0, 1.0); gl.glVertex3d(x + width, y,          z);
		gl.glEnd();

		y = y - height - space;
		
		reflectionTexture.bindTexture(gl);
		gl.glBegin(GL.GL_QUADS);
			gl.glTexCoord2d(0.0, 1.0); gl.glVertex3d(x,         y,          z);
			gl.glTexCoord2d(0.0, 0.0); gl.glVertex3d(x,         y - height, z);
			gl.glTexCoord2d(1.0, 0.0); gl.glVertex3d(x + width, y - height, z);
			gl.glTexCoord2d(1.0, 1.0); gl.glVertex3d(x + width, y,          z);
		gl.glEnd();
		
		gl.glDisable(GL.GL_TEXTURE_2D);
		gl.glEnable(GL.GL_LIGHTING);
	}
	
	public void setRotation(float[] rotation)
	{
		GL gl = World.getInstance().getGL();
		
		super.setRotation(rotation);
		
		gl.glMatrixMode(GL.GL_MODELVIEW);
		gl.glPushMatrix();
		gl.glLoadIdentity();
		
		gl.glRotatef(rotation[2], 0, 0, 1);
		gl.glRotatef(rotation[1], 0, 1, 0);
		gl.glRotatef(rotation[0], 1, 0, 0);
		
		double[] matrix = new double[16];
		
		gl.glGetDoublev(GL.GL_MODELVIEW_MATRIX, matrix, 0);
		
		VectorDouble defaultNormal = new VectorDouble(new double[] {0.0, 0.0, 1.0});

		for (int i = 0; i < 3; i++)
		{
			surfaceNormal.data[i] = 0.0;
			for (int j = 0; j < 3; j++)
			{
				surfaceNormal.data[i] += defaultNormal.data[j]*matrix[i + 4*j];
			}
		}
		
		gl.glPopMatrix();
	}
	
	public VectorFloat getSurfaceNormal()
	{
		return new VectorFloat(surfaceNormal.data);
	}
	
	public WaveGenerator getWaveGenerator() {
		return waveGenerator;
	}

	public void setWaveGenerator(WaveGenerator waveGenerator) {
		this.waveGenerator = waveGenerator;
	}
	
	public void setRender(boolean render) {
		this.render = render;
	}

	public boolean isRenderControlPoints() {
		return renderControlPoints;
	}

	public void setRenderControlPoints(boolean renderControlPoints) {
		this.renderControlPoints = renderControlPoints;
	}

}
